package com.zys.el.time.ga;

import cn.hutool.core.bean.BeanUtil;
import com.zys.el.time.utils.ExcelHandle;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;

@Slf4j
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TaskPlan {

    private SchedulePopulation sp;


    /**
     * 演化
     *
     * @param originPopulation 原始种群
     */
    public void evolution(List<Schedule> originPopulation) {
        // 初始化种群
        init_population(originPopulation);
        for (int i = 0; i < sp.maxIter; i++) {

            caleRate();

            mutate();

//            crossover();

        }
    }

    private HashMap<List<Integer>, Integer> findMaxRestDuration(List<Schedule> schedules, Schedule s) {
        List<Integer> wholeSlot = new ArrayList();
//        List<Integer> wholeMaxSlot = new ArrayList<>();
        int[] slots = new int[sp.scheduleNum];
        for (Schedule schedule : schedules) {
            if (schedule == s) continue;
            List<Integer> slot = schedule.getSlot();
            for (Integer i : slot) {
                slots[i]++;
            }
        }
        HashMap<List<Integer>, Integer> map = new HashMap<>();
        int maxSlot = 0, t = 0;
        for (int i = 0; i < slots.length; i++) {
            int slot = slots[i];
            if (slot == 0) {
                wholeSlot.add(i);
                t++;
            } else {
                if (t > 0) {
                    map.put(new ArrayList<>(wholeSlot), t);
                }
                if (t > maxSlot) {
//                    wholeMaxSlot.addAll(wholeSlot);
                    maxSlot = t;
                }
                t = 0;
                wholeSlot.clear();
            }
        }
        return map;
    }

    /**
     * 变异
     */
    public void mutate() {
        Random random = new Random();

        // 遍历每个种群
        for (List<Schedule> schedules : sp.population) {
            ArrayList<Schedule> timeDataCpy = new ArrayList<>(sp.originPop);
            for (Schedule schedule : schedules) {
                // 删除已安排的事件
                timeDataCpy.removeIf(timeData -> timeData.getTaskId().equals(schedule.getTaskId()));
            }


            double isDeleteProb = sp.mutProb; // 删除碱基概率
            double mutProb = sp.mutProb; // 变异概率
            double randomHappy = random.nextBoolean() ? (1 + Math.random()) : (1 - Math.random());

            double timeLenChangeProb = sp.mutProb * randomHappy; // 事件长度改变概率
            double longerChangeProb = sp.longerChangeProb; // 事件变长概率
            double timeMoveProb = sp.timeMoveProb; // 事件移动概率


            int setTimeRange = Schedule.isSetTimeRange(schedules);
            // 调整发生时间变长概率
            timeLenChangeProb += setTimeRange * 0.03 + (Schedule.wholeDayFullness(schedules, sp) - sp.originFullness) * 0.02;

            // 调整时间移动概率
            timeMoveProb += Schedule.hasConflict(schedules) * 0.06;

            // 碱基链太长调整变异概率
            /*int tasks = 4;
            while (schedules.size() > tasks) {
                tasks++;
                isDeleteProb *= (1 + this.mutProb * 0.4);
                mutProb *= (1 + this.mutProb * 0.2);
            }*/


            Iterator<Schedule> schIter = schedules.iterator();

            while (schIter.hasNext()) {
                Schedule schedule = schIter.next();
                double randomMutProb = Math.random();
                if (randomMutProb > mutProb) continue;

                // 调整时间变长概率
                int t = schedule.getTime() - schedule.getLenSlot() * 5;
                if (Math.abs(t) > Schedule.limitRange && t < 0) {
                    longerChangeProb *= (1 + 0.03 * t);
                } else if (Math.abs(t) > Schedule.limitRange && t > 0) {
                    longerChangeProb *= (1 - 0.03 * Math.abs(t));
                }

                // 随机拉长或缩短某碱基片
                if (Math.random() < timeLenChangeProb) {
                    int addTime = random.nextInt(2); // 扩充n个时间片
                    List<Integer> slot = schedule.getSlot();

                    Integer start = slot.get(0);
                    Integer end = slot.get(slot.size() - 1);
                    if (Math.random() < longerChangeProb) { // 扩充
                        if (random.nextBoolean()) { // 修改开始时间
                            for (int i = 1; i <= addTime; i++) {
                                if (start - i < 0) break; // 边界
                                slot.add(start - i);
                            }
                            Collections.sort(slot);
                        } else { // 修改结束时间
                            for (int i = 1; i <= addTime; i++) {
                                if (end + i >= sp.scheduleNum) break; // 边界
                                slot.add(end + i);
                            }
                        }
                    } else { // 收缩
                        if (random.nextBoolean()) { // 修改开始时间
                            Iterator<Integer> iter = slot.iterator();
                            int i = 0;
                            while (iter.hasNext() && i++ < addTime) {
                                if (slot.size() <= 1) break; // 边界
                                iter.next();
                                iter.remove();
                            }
                        } else { // 修改结束时间
                            for (int i = slot.size() - 1, j = 0; i >= 0 && j <= addTime; i--, j++) {
                                if (slot.size() <= 1) break; // 边界
                                slot.remove(i);
                            }
                        }
                    }
                    schedule.setLenSlot(slot.size());
                }

                // 调整位置

                /*if (Math.random() < timeMoveProb *0.5) {
                    int movieSeed = (int) (1.5 * Schedule.hasConflict(schedules)) + 1;
                    int randomMove = random.nextInt(movieSeed);
                    if (random.nextBoolean()) { // 往上小幅度移动
                        List<Integer> slot = schedule.getSlot();
                        Integer start = slot.get(0);
                        if (start - randomMove >= 0) {
                            schedule.setSlot(slot.stream().map(v -> v - randomMove).collect(Collectors.toList()));
                        } else {
                            schedule.setSlot(slot.stream().map(v -> v - start).collect(Collectors.toList()));
                        }

                    } else { // 往下小幅度移动
                        List<Integer> slot = schedule.getSlot();
                        Integer end = slot.get(slot.size() - 1);
                        if (end + randomMove < scheduleNum) {
                            schedule.setSlot(slot.stream().map(v -> v + randomMove).collect(Collectors.toList()));
                        } else {
                            int move = scheduleNum - 1 - end;
                            schedule.setSlot(slot.stream().map(v -> v + move).collect(Collectors.toList()));
                        }
                    }
                }*/

                // 移动到其他位置
                if (Math.random() < timeMoveProb) {
                    HashMap<List<Integer>, Integer> freeSlotMap = findMaxRestDuration(schedules, schedule);
                    Set<Map.Entry<List<Integer>, Integer>> entries = freeSlotMap.entrySet();
                    ArrayList<Map.Entry<List<Integer>, Integer>> entryList = new ArrayList<>(entries);
                    // 排序
                    entryList.sort((o1, o2) -> o2.getValue().compareTo(o1.getValue()));

                    for (Map.Entry<List<Integer>, Integer> entry : entryList) {
                        // 空闲数组
                        List<Integer> freeSlots = entry.getKey();
                        Integer size = entry.getValue();
                        int lenSlot = schedule.getLenSlot();

                        // hashmap按倒序排
                        if (!freeSlots.isEmpty()) {
                            if (size < lenSlot + 4) {
                                break;
                            }
                        }
                        if (!freeSlots.isEmpty() && freeSlots.get(2) + lenSlot >= sp.scheduleNum) continue;

                        if (!freeSlots.isEmpty()) {
                            Integer idx = freeSlots.get(random.nextInt(freeSlots.size() - lenSlot - 2) + 2);
                            List<Integer> slot = schedule.getSlot();
                            int changeMove = slot.get(0) - idx;
                            for (int i = 0; i < slot.size(); i++) {
                                slot.set(i, slot.get(i) - changeMove);
                            }

                            break;
                        }
                    }
                }
                /// 随机变换位置
                /*if (Math.random() < timeMoveProb) {
                    int randomIdx = random.nextInt(scheduleNum);
                    int lenSlot = schedule.getLenSlot();

                    if (randomIdx + lenSlot >= scheduleNum)
                        randomIdx -= randomIdx + lenSlot - scheduleNum + 1;

                    ArrayList<Integer> newSlot = new ArrayList<>();
                    for (int i = 1; i <= lenSlot; i++) {
                        newSlot.add(randomIdx + i);
                    }
                    schedule.setSlot(newSlot);
                }*/

                // 是否需要排完
                /*if (!sp.isArrangeFull) {
                    double randomDelete = Math.random();
                    if (isDeleteProb > randomDelete) { // 删除某片碱基
                        timeDataCpy.add(new Schedule(schedule));
                        schIter.remove();

                    } else if (!timeDataCpy.isEmpty() && isDeleteProb <= randomDelete) { // 变异某个碱基片
                        Schedule newSch = timeDataCpy.get(random.nextInt(timeDataCpy.size()));
                        String tmpTaskId = schedule.getTaskId();
                        schedule.setTaskId(newSch.getTaskId());
                        newSch.setTaskId(tmpTaskId);

                    }

                }*/
            }
        }

    }

    /**
     * 交叉
     */
    public void crossover() {
        Random random = new Random();

        Iterator<List<Schedule>> iter = sp.population.iterator();
        while (iter.hasNext()) {
            List<Schedule> newCrossScList = iter.next();
            double rate = Math.random();
            if (rate < sp.crossProb) {
                // 如果不存在此种群，则任选一个此种群中不存在的个体进行交叉，交叉事件和时间片
                int idx = random.nextInt(sp.population.size());
                List<Schedule> sc = sp.population.get(idx);
                Schedule newToCrossSc = newCrossScList.get(random.nextInt(newCrossScList.size()));

                Schedule crossSc = sc.get(random.nextInt(sc.size()));

//                toCrossSc.setTaskId(crossSc.getTaskId());
                newToCrossSc.setTime(crossSc.getTime());
                newToCrossSc.setLenSlot(crossSc.getLenSlot());
                newToCrossSc.setSlot(new ArrayList<>(crossSc.getSlot()));

            }
        }
    }


    /**
     * 初始化
     *
     * @param originPopulation 原始种群
     */
    public void init_population(List<Schedule> originPopulation) {
        sp.population = new ArrayList<>();
        Random random = new Random();
        for (int i = 0; i < sp.popSize; i++) {
            List<Schedule> entity = new ArrayList<>();
            for (int j = 0; j < originPopulation.size(); j++) {
                Schedule tmp = originPopulation.get(j);
                tmp.random_init(sp);

                // 添加进种群
                Schedule schedule = new Schedule(tmp.getTaskId(), tmp.getSlot(), tmp.getTime(), tmp.getLenSlot());
                entity.add(schedule);
//                // 有冲突则把刚才的移除掉
//                if (Schedule.hasConflict(entity)) {
//                    entity.remove(schedule);
//                }
            }
            sp.population.add(entity);
        }
    }


    /**
     * 计算适应度
     */
    public void caleRate() {
        HashMap<List<Schedule>, Double> map = new HashMap<>();
        // 自定义硬性规则
        Schedule.checkHardness(map, sp.population);


        // 计算跨越休息时间的事件适应度
        HashSet<Integer> set = new HashSet<>();
        for (List<Schedule> schedules : sp.population) {
            int unqualifiedSpanning = 0;
            int submit = 0;
            int noRestTime = 0;
            set.clear();

            // 总时间表
            int[] wholeSlot = new int[sp.scheduleNum]; // 判断是否有休息时间
            for (Schedule schedule : schedules) {
                List<Integer> slot = schedule.getSlot();
                set.addAll(slot);
                int n = slot.size();
                Integer start = slot.get(0);
                Integer end = slot.get(n - 1);
                if ((end > 47 && start < 47) || (end > 106 && start < 107)) {
                    if (end - 48 < 47 - start || end - 107 < 106 - start) {
                        if (end - 48 < 4 || end - 107 < 4) { // 跨越时间少于20分钟
                            unqualifiedSpanning++;
                        }
                    }
                } else {
                    submit++;
                }

                // 判断是否有休息时间
                for (int i = 1; i <= sp.restTime; i++) {
                    boolean f = false;
                    if (start - i >= 0 && wholeSlot[start - i] != 0) {
                        noRestTime++;
                        f = true;
                    }
                    if (end + i < sp.scheduleNum && wholeSlot[end + i] != 0) {
                        noRestTime++;
                        f = true;
                    }
                    if (f) break;
                }
                // 添加休息时间
                for (Integer i : slot) {
                    if (i >= sp.scheduleNum) {
                        System.out.println(slot);
                        System.out.println(schedule);
                    }
                    wholeSlot[i]++;
                }
            }

            Double rate = map.get(schedules)
                    - sp.hardnessProb * unqualifiedSpanning // 跨越休息时间
                    - sp.hardnessProb * noRestTime // 没有休息时间
                    + submit * 0.2 // 非跨越休息时间
                    + (double) set.size() / sp.scheduleNum * 2; // 安排全天饱满度
            map.put(schedules, rate);
        }
        // 按适应度排序
        ArrayList<Map.Entry<List<Schedule>, Double>> entries = new ArrayList<>(map.entrySet());
        entries.sort((o1, o2) -> o2.getValue().compareTo(o1.getValue()));

        List<List<Schedule>> newPopulation = new ArrayList<>();
        // 赋值回种群集
        int talentNum = sp.popSize / 4;
        for (int i = 0; i < talentNum; i++) {
            List<Schedule> schedules = entries.get(i).getKey();
            newPopulation.add(schedules);
        }
        sp.population = newPopulation;

        // 轮盘赌 种群规模上限-精英数 次
        /*int roundNum = this.popSize - talentNum;
        for (int i = 0; i < roundNum; i++) {
            double rate = Math.random();
            if (rate < this.newSpeciesProb) {
                Random random = new Random();
                List<Schedule> schedules = this.population.get(random.nextInt(this.population.size() - 1));

                // 添加新基因位

                // 判断时长是否大于30分钟
                Schedule maxLenSch = schedules.get(0);
                for (int j = 1; j < schedules.size(); j++) {
                    if (maxLenSch.getLenSlot() > schedules.get(j).getLenSlot()) {
                        maxLenSch = schedules.get(j);
                    }
                }
                int lenSlot = maxLenSch.getLenSlot();
                if (lenSlot > 6) { // 大于30分钟才产生新基因位
                    // 获取此种群没有的新基因
                    List<Schedule> notContainSchedules = this.getNotContainSchedule(schedules);
                    // 新基因位
                    int idx = random.nextInt(notContainSchedules.size());

                    List<Integer> maxLenSchSlot = maxLenSch.getSlot();
                    Schedule newSch = notContainSchedules.get(idx);
                    int newLenSlot = random.nextInt(lenSlot - 3) + 1;
                    newSch.setLenSlot(newLenSlot);

                    // 新基因占位
                    ArrayList<Integer> newSlot = new ArrayList<>();
                    Integer slotPlace;

                    int startOrEnd = random.nextInt(2);
                    if (startOrEnd == 0) { // 从当前基因位的开始时间往后截取
                        // 旧基因位位置
                        slotPlace = maxLenSchSlot.get(0);
                        // 新基因位
                        for (int j = 0; j < newLenSlot; j++)
                            newSlot.add(slotPlace + j);

                    } else { // 从当前基因位的结束时间往前截取
                        // 旧基因位位置
                        slotPlace = maxLenSchSlot.get(maxLenSchSlot.size() - 1);
                        // 新基因位
                        for (int j = 0; j < newLenSlot; j++)
                            newSlot.add(slotPlace - j);

                    }
                    // 添加新基因占位
                    newSch.setSlot(newSlot);
                    newSch.setLenSlot(newSlot.size());
                    newSch.setTime(5 * newSch.getLenSlot());
                    // 移除旧基因占的位置
                    maxLenSchSlot.removeAll(newSlot);
                    maxLenSch.setLenSlot(maxLenSch.getLenSlot() - newSch.getLenSlot());
                    maxLenSch.setTime(maxLenSch.getTime() - newSch.getTime());
                }

            }
        }*/
    }

    /**
     * 获取未安排的任务
     *
     * @param schedule 已安排的任务种群
     * @return
     */
    public List<Schedule> getNotContainSchedule(List<Schedule> schedule) {
        ArrayList<Schedule> res = new ArrayList<>();
        HashMap<String, Integer> map = new HashMap<>();
        HashMap<String, Schedule> oriMap = new HashMap<>();
        // 初始化map
        sp.originPop.forEach(o -> map.put(o.getTaskId(), 0));
        sp.originPop.forEach(s -> oriMap.put(s.getTaskId(), s));

//        System.out.println(map);
        // 打点计数
        schedule.forEach(s -> map.put(s.getTaskId(), map.get(s.getTaskId()) + 1));

        ArrayList<Map.Entry<String, Integer>> entries = new ArrayList<>(map.entrySet());
        entries.sort(Map.Entry.comparingByValue());

        for (Map.Entry<String, Integer> entry : entries) {
            if (entry.getValue() == 0) {
                Schedule s = oriMap.get(entry.getKey());
                res.add(BeanUtil.copyProperties(s, Schedule.class));
            } else {
                break;
            }
        }

        return res;
    }

    public static List<List<Schedule>> doPlan(List<Task> tasks, boolean isViewByExcel) {
        SchedulePopulation sp = new SchedulePopulation();
        TaskPlan taskPlan = new TaskPlan(sp);

        int originSlotNum = 0;
        List<Schedule> schedules = new ArrayList<>();
        for (Task t : tasks) {
            originSlotNum += t.getTime() / 5;
            schedules.add(new Schedule(t.getTaskId(), t.getTime()));
        }

        // 赋值到初始化种群中
        sp.originPop = schedules;

        sp.originFullness = (double) originSlotNum / sp.scheduleNum * 100;
        System.out.println("初始饱满度：" + sp.originFullness + "%");

        log.info(String.valueOf(schedules));

        // 开始演化
        taskPlan.evolution(schedules);

        if (isViewByExcel) {
            XSSFWorkbook wb = new XSSFWorkbook();
            int i = 2;
//        ExcelHandle.exportData(wb, sp.originPop, i++); // 初始数据
            System.out.println("---------------========---------------");
            for (List<Schedule> scheduleList : sp.population) {
                System.out.println("***==***");
                System.out.println(scheduleList);
                System.out.println("个数：" + scheduleList.size());

                // 转换为excel表
                try {
                    ExcelHandle.exportData(wb, scheduleList, i++);
                } catch (IOException e) {
                    log.error(e.getMessage());
                }

                double wholeDayFullness = Schedule.wholeDayFullness(scheduleList, sp);
                System.out.println("安排饱满度：" + wholeDayFullness * 100 + "%");
                System.out.println("***==***");
            }
            System.out.println("---------------========---------------");
            try {
                FileOutputStream fileOut = new FileOutputStream("taskPlan.xlsx");
                wb.write(fileOut);
                fileOut.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return sp.population;
    }

    /*public static void main(String[] args) throws IOException {
        SchedulePopulation sp = new SchedulePopulation();
        TaskPlan myGa = new TaskPlan(sp);
        ArrayList<Schedule> schedules = new ArrayList<>();
        Random random = new Random(System.currentTimeMillis());
        int originSlotNum = 0;
        for (int i = 0; i < 10; i++) {
            int time = random.nextInt(50) + 20;
            schedules.add(new Schedule("Task_" + i, time));
            originSlotNum += time / 5;
        }

        sp.originFullness = (double) originSlotNum / sp.scheduleNum * 100;
        System.out.println("初始饱满度：" + sp.originFullness + "%");

        sp.originPop = schedules;

        // 开始演化
        myGa.evolution(schedules);
//        System.out.println(sp.originPop);

        XSSFWorkbook wb = new XSSFWorkbook();
        int i = 2;
//        ExcelHandle.exportData(wb, sp.originPop, i++); // 初始数据
        System.out.println("---------------========---------------");
        for (List<Schedule> scheduleList : sp.population) {
            System.out.println("***==***");
            System.out.println(scheduleList);
            System.out.println("个数：" + scheduleList.size());

            // 转换为excel表
            ExcelHandle.exportData(wb, scheduleList, i++);

            double wholeDayFullness = Schedule.wholeDayFullness(scheduleList, sp);
            System.out.println("安排饱满度：" + wholeDayFullness * 100 + "%");
            System.out.println("***==***");
        }
        System.out.println("---------------========---------------");
        FileOutputStream fileOut = new FileOutputStream("workbook.xlsx");
        wb.write(fileOut);
        fileOut.close();
    }*/
}